package com.lmn.dao;

import com.lmn.dao.model.Test;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.sql.Statement;
import java.util.Map;
import java.util.Properties;

/**
 * mybatis 拦截器默认可拦截的类型只有四种，
 * Executor：拦截执行器的方法。
 * ParameterHandler：拦截参数的处理。
 * ResultHandler：拦截结果集的处理。
 * StatementHandler：拦截Sql语法构建的处理。
 * 对于我们的自定义拦截器必须使用 mybatis 提供的注解来指明我们要拦截的是四类中的哪一个类接口，具体规则如下：
 * a：Intercepts 拦截器: 标识我的类是一个拦截器
 * b：Signature 署名: 则是指明我们的拦截器需要拦截哪一个接口的哪一个方法
 * type 对应四类接口中的某一个，比如是 Executor
 * method 对应接口中的哪类方法，比如 Executor 的 update 方法
 * args 对应接口中的哪一个方法，比如 Executor 中 query 因为重载原因，方法有多个，args 就是指明参数类型，从而确定是哪一个方法
 */
@Intercepts({
        @Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class}),
        @Signature(method = "query", type = StatementHandler.class, args = {Statement.class, ResultHandler.class})
})
@Component
public class MyInterceptor implements Interceptor {
    private static final Logger log = LoggerFactory.getLogger(MyInterceptor.class);

    /**
     * 这个方法很好理解
     * 作用只有一个：我们不是拦截方法吗，拦截之后我们要做什么事情呢？
     * 这个方法里面就是我们要做的事情
     * <p>
     * 解释这个方法前，我们一定要理解方法参数 {@link Invocation} 是个什么鬼？
     * 1 我们知道，mybatis拦截器默认只能拦截四种类型 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler
     * 2 不管是哪种代理，代理的目标对象就是我们要拦截对象，举例说明：
     * 比如我们要拦截 {@link Executor#update(MappedStatement ms, Object parameter)} 方法，
     * 那么 Invocation 就是这个对象，Invocation 里面有三个参数 target method args
     * target 就是 Executor
     * method 就是 update
     * args   就是 MappedStatement ms, Object parameter
     * <p>
     * 如果还是不能理解，我再举一个需求案例：看下面方法代码里面的需求
     * <p>
     * 该方法在运行时调用
     */
    @Override
    @SuppressWarnings("unchecked")
    public Object intercept(Invocation invocation) throws Throwable {
        // 根据签名指定的args顺序获取具体的实现类
        // 1. 获取MappedStatement实例, 并获取当前SQL命令类型
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        SqlCommandType commandType = ms.getSqlCommandType();

        // 2. 获取当前正在被操作的类, 有可能是Java Bean, 也可能是普通的操作对象, 比如普通的参数传递
        // 普通参数, 即是 @Param 包装或者原始 Map 对象, 普通参数会被 Mybatis 包装成 Map 对象
        // 即是 org.apache.ibatis.binding.MapperMethod$ParamMap
        Object parameter = invocation.getArgs()[1];
        // 获取拦截器指定的方法类型, 通常需要拦截 update
        String methodName = invocation.getMethod().getName();
        log.info("NormalPlugin, methodName; {}, commandType: {}", methodName, commandType);

        // 3. 获取当前用户信息
        Test user = new Test();
        if (parameter instanceof Test) {
            // 4. 实体类
            Test entity = (Test) parameter;
            if (user != null) {
                user.setId(entity.getId());
            }
            if (methodName.equals("update")) {
                if (commandType.equals(SqlCommandType.INSERT)) {
                    entity.setCreateTime(System.currentTimeMillis());
                    entity.setUpdateTime(System.currentTimeMillis());
                } else if (commandType.equals(SqlCommandType.UPDATE)) {
                    entity.setUpdateTime(System.currentTimeMillis());
                }
            }
        } else if (parameter instanceof Map) {
            // 5. @Param 等包装类
            // 更新时指定某些字段的最新数据值
            if (commandType.equals(SqlCommandType.UPDATE)) {
                // 遍历参数类型, 检查目标参数值是否存在对象中, 该方式需要应用编写有一些统一的规范
                // 否则均统一为实体对象, 就免去该重复操作
                Map map = (Map) parameter;
                if (map.containsKey("creator")) {
                    map.put("creator", "---");
                }
                if (map.containsKey("updateTime")) {
                    map.put("updateTime", System.currentTimeMillis());
                }
            }
        }
        // 6. 均不是需要被拦截的类型, 不做操作
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        System.out.println(properties.toString());
    }
}
